package backend

import (
	"bytes"
	"html/template"
	"lanmon/internal/config"
	"lanmon/internal/utils"
	"time"

	paho "github.com/eclipse/paho.mqtt.golang"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
)

//
// MQTT lanmon-client
type MqttClient struct {
	conn           paho.Client
	eventTopic     string
	broadcastTopic string
	commandTopic   string
	qos            uint8
	server         string

	clientId string
}

func setupMqttClient(c *config.Config) (*MqttClient, error) {
	conf := &c.MQTT

	ipv4, err := utils.GetLocalIp()
	if err != nil {
		return nil, err
	}

	b := &MqttClient{
		eventTopic:     conf.EventTopic,
		broadcastTopic: conf.BroadcastTopic,
		qos:            conf.QOS,
		server:         conf.Server,
		clientId:       ipv4,
	}

	commandTopicTemplate, err := template.New("command").Parse(conf.CommandTopicTemplate)
	if err != nil {
		return nil, errors.Wrap(err, "parse command topic template error")
	}

	templateCtx := struct {
		ClientId string
	}{b.clientId}
	topic := bytes.NewBuffer(nil)
	if err := commandTopicTemplate.Execute(topic, templateCtx); err != nil {
		return nil, errors.Wrap(err, "execute command topic template error")
	}

	b.commandTopic = topic.String()

	logoutEvent := newMqttEvent(b.clientId, EventLogout, nil)
	logoutPayload, err := logoutEvent.toJson()
	if err != nil {
		return nil, errors.Wrap(err, "parse logout event error")
	}

	opts := paho.NewClientOptions()
	opts.AddBroker(b.server)
	opts.SetUsername(conf.Username)
	opts.SetPassword(conf.Password)
	opts.SetCleanSession(conf.CleanSession)
	opts.SetClientID(conf.ClientID)
	opts.SetOnConnectHandler(b.onConnected)
	opts.SetConnectionLostHandler(b.onConnectionLost)
	opts.SetMaxReconnectInterval(conf.MaxReconnectInterval)
	opts.SetWill(b.eventTopic, logoutPayload, b.qos, false)

	log.WithField("server", b.server).Info("connecting to mqtt broker")
	b.conn = paho.NewClient(opts)
	for {
		if token := b.conn.Connect(); token.Wait() && token.Error() != nil {
			log.Errorf("connecting to mqtt broker failed, will retry: %s", token.Error())
			time.Sleep(5 * time.Second)
		} else {
			break
		}
	}

	return b, nil
}

func (b *MqttClient) onConnected(c paho.Client) {
	log.Info("connected to mqtt server")
	b.connect()
}

func (b *MqttClient) onConnectionLost(c paho.Client, reason error) {
	b.logout()
	log.Errorf("mqtt connection error: %s", reason)
}

func (b *MqttClient) connect() error {
	for {
		log.WithFields(log.Fields{
			"topic": b.commandTopic,
			"qos":   b.qos,
		}).Info("subscribing to gateway command topic")
		if token := b.conn.Subscribe(b.commandTopic, b.qos, b.commandHandler); token.Wait() && token.Error() != nil {
			log.WithError(token.Error()).WithFields(log.Fields{
				"topic": b.commandTopic,
				"qos":   b.qos,
			}).Errorf("subscribe error")
			time.Sleep(time.Second)
			continue
		}
		break
	}

	for {
		log.WithFields(log.Fields{
			"topic": b.broadcastTopic,
			"qos":   b.qos,
		}).Info("subscribing to gateway broadcast topic")
		if token := b.conn.Subscribe(b.broadcastTopic, b.qos, b.broadcastHandler); token.Wait() && token.Error() != nil {
			log.WithError(token.Error()).WithFields(log.Fields{
				"topic": b.broadcastTopic,
				"qos":   b.qos,
			}).Errorf("subscribe error")
			time.Sleep(time.Second)
			continue
		}
		break
	}

	for {
		log.Info("client login")
		if err := b.login(); err != nil {
			time.Sleep(time.Second)
			continue
		}
		break
	}

	return nil
}

func (b *MqttClient) login() error {
	args := make(map[string]interface{})
	// TODO: upload more info
	args["sysinfo"] = *utils.GetSysInfo()
	return b.publishEvent(newMqttEvent(b.clientId, EventLogin, args))
}

func (b *MqttClient) logout() error {
	return b.publishEvent(newMqttEvent(b.clientId, EventLogout, nil))
}

func (b *MqttClient) disconnect() error {
	b.logout()
	go func() {
		if b.conn.IsConnected() {
			b.conn.Disconnect(250)
		}
	}()
	log.Info("disconnect")
	return nil
}

func (b *MqttClient) broadcastHandler(c paho.Client, msg paho.Message) {
	payload := string(msg.Payload())
	log.WithFields(log.Fields{
		"topic":   msg.Topic(),
		"payload": payload,
	}).Info("handle broadcast")
	b.dispatchBroadcast(payload)
}

func (b *MqttClient) dispatchBroadcast(payload string) error {
	var broadcast MqttBroadcast
	if err := broadcast.fromJson(payload); err != nil {
		return err
	}

	// TODO:
	switch broadcast.Action {
	case BroadcastDied:
		return nil
	case BroadcastClientLogin:
		return b.login()
	case BroadcastShutdown:
		return b.shutdown(broadcast.Args)
	}
	return nil
}

func (b *MqttClient) publishEvent(event *MqttEvent) error {
	if !b.conn.IsConnected() {
		return errors.New("do not connect to server")
	}
	payload, err := event.toJson()
	if err != nil {
		return errors.Wrap(err, "convert event error")
	}

	if token := b.conn.Publish(b.eventTopic, b.qos, false, []byte(payload)); token.Wait() && token.Error() != nil {
		return errors.Wrap(token.Error(), "publish gateway command error")
	}
	return nil
}

func (b *MqttClient) commandHandler(c paho.Client, msg paho.Message) {
	payload := string(msg.Payload())
	log.WithFields(log.Fields{
		"topic":   msg.Topic(),
		"payload": payload,
	}).Info("handle command")
	b.dispatchCommand(payload)
}

func (b *MqttClient) dispatchCommand(payload string) error {
	var command MqttCommand
	if err := command.fromJson(payload); err != nil {
		return err
	}

	// TODO:
	switch command.Action {
	case CommandShutdown:
		return b.shutdown(command.Args)
	case CommandClientLogin:
		return b.login()
	}
	return nil
}

func (b *MqttClient) shutdown(args map[string]interface{}) error {
	b.disconnect()
	time.Sleep(1000 * time.Millisecond) // let Server handle logout

	var reboot = false
	if args != nil {
		val, exist := args["reboot"]
		if exist && val == 1 {
			reboot = true
		}
	}

	if reboot {
		return utils.ShellExec("reboot")
	}

	return utils.ShellExec("shutdown -h now")
}
